import os
import md5
import sys
import time
import socket
import irclib
import getopt
import logging
import logging.handlers
from datetime import datetime, timedelta
import locale

from arkivo import model
from tg.config import Bunch

# A pure python version of Python 2.5's defaultdict
# Source: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/523034
try:
    from collections import defaultdict
except:
    class defaultdict(dict):
        
        def __init__(self, default_factory=None, *a, **kw):
            if (default_factory is not None and
                not hasattr(default_factory, '__call__')):
                raise TypeError('first argument must be callable')
            dict.__init__(self, *a, **kw)
            self.default_factory = default_factory
        
        def __getitem__(self, key):
            try:
                return dict.__getitem__(self, key)
            except KeyError:
                return self.__missing__(key)
        
        def __missing__(self, key):
            if self.default_factory is None:
                raise KeyError(key)
            self[key] = value = self.default_factory()
            return value
        
        def __reduce__(self):
            if self.default_factory is None:
                args = tuple()
            else:
                args = self.default_factory,
            return type(self), args, None, None, self.iteritems()
        
        def copy(self):
            return self.__copy__()
        
        def __copy__(self):
            return type(self)(self.default_factory, self)
        
        def __deepcopy__(self, memo):
            import copy
            return type(self)(self.default_factory,
                              copy.deepcopy(self.items()))
        
        def __repr__(self):
            return 'defaultdict(%s, %s)' % (self.default_factory,
                                            dict.__repr__(self))

def _default_cache_time():
    """This function returns datetime.now()
    for a defaultdict instance"""
    from datetime import datetime
    return datetime.now()

class ArkivoClient(object):
    """A not-so-simple multiple server IRC client class.
    
    Based on the SimpleIRCClient class in irclib, but with extras.
    
    Doesn't deal with DCC in the slightest."""
    
    def __init__(self):
        self.ircobj = irclib.IRC()
        self.connections = {}
        self.connection_details = {}
        self.ircobj.add_global_handler("all_events", self._dispatcher, -10)
        
        self.services = Bunch()
        self.services.cache_delay = 60*5
        self.services.cache_delay_timedelta = timedelta(seconds=self.services.cache_delay)
        self.services.cache_time = defaultdict(_default_cache_time)
        self.services.objects = dict()
        self.services.current = list()
        
    def _dispatcher(self, connection, event):
        """Dispatches events to the handler of the same event name (with a prefix of 'on_')"""
        handler = "on_%s" % event.eventtype()
        if hasattr(self, handler):
            getattr(self, handler)(connection, event)
            
    def create_connection(self, service, server, port, nick, password=None, username=None,
                ircname=None, localaddress="", localport=0):
        """Add a service to the connection dict for later use.
        Defunct now I'm pulling everything from the DB"""
        if not service in self.connections.keys():
            self.connection_details[service] = dict(server=server, port=port, nickname=nick,
                                            password=password, username=username,
                                            ircname=ircname, localaddress=localaddress,
                                            localport=0)
            self.connections[service] = self.ircobj.server()
                                            
    def remove_connection(self, service):
        pass
        
    def get_service_name(self, connection):
        """Retreives a service name for a given connection"""
        for k,v in self.connections.items():
            if v == connection:
                return k
        return None
        
    def get_service_object(self, service):
        if not service in self.services.objects.keys() or \
            self.services.cache_time[service] < datetime.now() - self.services.cache_delay_timedelta:
            self.services.objects[service] = model.Service.by_name(service)
        
        if not self.services.objects[service]:
            raise ValueError("Service '%s' could not be found" % str(service))
        return self.services.objects[service]
                                            
    def connect_service(self, service):
        """Connect/reconnect a particular service"""
        db_service = model.Service.by_name(service)
        if db_service:
            d = db_service.connection_dict
            self.connections[service] = self.ircobj.server()
            self.connections[service].connect(**d)
            self.services.current.append(service)

                        
    def connect(self):
        """Connect/reconnect to all available services"""
        for service in model.Service.query.all():
            self.connect_service(service.name)
            
    def start(self):
        """Start the client"""
        self.ircobj.process_forever()
        
        
class ArkivoBot(ArkivoClient):
    
    def __init__(self):
        super(ArkivoBot, self).__init__()
        # Remove below in favour of super
        # ArkivoClient.__init__(self)
        self.inmotd = dict()
        self.alt_nick = ''
        ident = md5.new('arkivobot md5 salt')
        ident.update(str(time.time()))
        self.ident = ident.hexdigest()
        logging.info("IDENT %s" % self.ident)
        self.last_check = {}
        self.delay = 30
        self.alternate = defaultdict(str)
        
        self.shutdown_init = False
        
        self.connection_channels = {}
        self.current_channels = defaultdict(list)
        self.nicklist = defaultdict(lambda: defaultdict(list))
        
    def connect(self):
        try:
            super(ArkivoBot, self).connect()
            # ArkivoClient.connect(self)
            setting = model.Setting.by_name("is_online")
            if not setting:
                setting = model.Setting()
                setting.name = "is_online"
            
            setting.data = True
            model.DBSession.flush()
            
            return True
        except irclib.ServerConnectionError:
            logging.error('Could not connect to one or more services')
            return False
            
    def shutdown(self):
        self.shutdown_init = True
        for service in [s.name for s in model.Service.query.all()]:
            connection = self.connections[service]
            try:
                connection.quit("Bot shutting down.  Bye bye!")
            except irclib.ServerNotConnectedError:
                pass
        model.Setting.by_name("is_online").data = False
        model.DBSession.flush()
            
    def latest_channels(self, service):
        service = model.Service.by_name(service)
        channels = service.enabled_channels
        return [chan.name for chan in channels]
        
    def join_channels(self, service, channels):
        db_service = model.Service.by_name(service)
        self.connection_channels[service] = self.latest_channels(service)
        logging.debug("connection_channels for %s: %s" % (service, self.connection_channels[service]))
        logging.debug("current_channels for %s: %s" % (service, channels))
        
        for c in channels:
           if c not in self.connection_channels[service]:
               logging.info('PARTing removed channel %s on %s' % (c, service))
               self.connections[service].part('#'+c, "(http://irclog.turbogears.org)")
        
        for c in self.connection_channels[service]:
           if c not in channels:
               logging.info('JOINing missing channel %s on %s' % (c, service))
               self.connections[service].join('#'+c)
               
        self.last_check[service] = time.time()
    
    def check_online(self, connection):
        """Does a little online check"""
        connection.privmsg(connection.get_nickname(), self.ident)
        connection.whois([connection.get_nickname()])
        #check for new services
        for service in [s.name for s in model.Service.query.all()]:
            if service not in self.services.current: self.connect_service(service)

        connection.execute_delayed(self.delay, self.check_online, [connection])
       
    def check_nick(self, connection):
        service = self.get_service_name(connection)
        nick = self.get_service_object(service).nickname
        if connection.get_nickname() != nick:
            connection.ison([nick])
        
    def update_nicks(self, connection):
        # Run a WHO command for each channel
        # and make sure the nicklist is up to date
        service = self.get_service_name(connection)
        for channel in self.current_channels[service]:
            connection.who("#%s" % channel)
            
        connection.execute_delayed(60*5, self.update_nicks, [connection])
        
    ###
    # Event handlers
    ###
    def on_ison(self, connect, event):
        service = self.get_service_name(connect)
        nick = self.get_service_object(service).nickname
        if nick not in [n.strip() for n in event.arguments()]:
            logging.warn('%s::Nick %s now available', nick)
            self.alternate[service] = ''
            connect.nick(nick)
            
    def on_nicknameinuse(self, connect, event):
        service = self.get_service_name(connect)
        if not self.get_service_object(service):
            logging.info("on_nicknameinuse::cannot find service %s" % service)
        nick = self.get_service_object(service).nickname
        self.alternate[service] += '_'
        connect.nick(nick+self.alternate[service])
        
    def on_whoischannels(self, connect, event):
        service = self.get_service_name(connect)
        channels = [c[1:].strip() for c in event.arguments()[1].split() if c.startswith('#')]
        self.join_channels(service, channels)
        
    def on_whoisuser(self, connect, event):
        service = self.get_service_name(connect)
        logging.debug("%s::WHOISUSER %s" % (service, event.target()))
        if service not in self.last_check.keys():
            self.last_check[service] = 0
        if time.time() > self.last_check[service] + 3*self.delay:
            logging.warn('Refreshing channel info for %s' % service)
            self.join_channels(service, [])
            
    def on_motd(self, connect, event):
        service = self.get_service_name(connect)
        if service not in self.inmotd.keys():
            self.inmotd[service] = False
        if not self.inmotd[service]:
            logging.info('%s::MOTD started' % service)
            self.inmotd[service] = True

    def on_endofmotd(self, connect, event):
        service = self.get_service_name(connect)
        self.inmotd[service] = False
        logging.info('%s::MOTD ended, join channels...' % service)
        connect.ison([self.get_service_object(service).nickname])
        self.check_online(connect)
        self.update_nicks(connect)
        
    def on_privmsg(self, connect, event):
        service = self.get_service_name(connect)
        if ''.join(event.arguments()) == self.ident:
            logging.debug('%s::check_online successful' % service)
            
            last_online = model.Setting.by_name("last_online")
            if not last_online:
                last_online = model.Setting()
                last_online.name = "last_online"
            last_online.data = datetime.utcnow()
            model.DBSession.flush()
        else:
            logging.warn('%s::PRIVMSG %s' % (service, ''.join(event.arguments())))
            
    def on_join(self, connect, event):
        service = self.get_service_name(connect)
        nick = event.source().split('!')[0]
        channel = event.target()[1:]
        
        if nick == self.get_service_object(service).nickname+self.alternate[service]:
            if channel not in self.current_channels[service]:
                self.current_channels[service].append(channel)
            logging.info('JOINed %s on %s' % (event.target(), service))
        else:
            self.log_message(service, nick, channel, u'joined the channel', u'join')
            if not channel in self.nicklist[service][nick]:
                self.nicklist[service][nick].append(channel)
            
    def on_part(self, connect, event):
        service = self.get_service_name(connect)
        nick = event.source().split('!')[0]
        channel = event.target()[1:]
        
        self.log_message(service, nick, channel, u'left the channel', u'part')
        if channel in self.nicklist[service][nick]:
            self.nicklist[service][nick].remove(channel)
            
    def on_quit(self, connect, event):
        service = self.get_service_name(connect)
        nick = event.source().split('!')[0]
        message = event.arguments()[0]
        
        channels = list(self.nicklist[service][nick])
        del self.nicklist[service][nick]
        for channel in channels:
            self.log_message(service, nick, channel, message, u'quit')
        
    def on_whoreply(self, connect, event):
        service = self.get_service_name(connect)
        channel = event.arguments()[0][1:]
        nick = event.arguments()[4]
        if not channel in self.nicklist[service][nick]:
            self.nicklist[service][nick].append(channel)
            
    def on_namreply(self, connect, event):
        service = self.get_service_name(connect)
        op_token = event.arguments()[0]
        channel = event.arguments()[1][1:]
        for n in event.arguments()[2].split(" "):
            if n.startswith(op_token):
                n = n[1:]
            if not channel in self.nicklist[service][n]:
                self.nicklist[service][n].append(channel)
            
    def on_topic(self, connect, event):
        service = self.get_service_name(connect)
        logging.info("%s::CHANGETOPIC %s :: %s" % (service, event.target(), ''.join(event.arguments())))
        chan = event.target().split('#')[1]
        chan = model.Channel.by_name_and_service(chan, service)
        chan.topic = ''.join(event.arguments())
        model.DBSession.flush()

    def on_currenttopic(self, connect, event):
        service = self.get_service_name(connect)
        chan = model.Channel.by_name_and_service(event.arguments()[0].split('#')[1], service)
        chan.topic = event.arguments()[1]
        logging.info("%s::CURRENTTOPIC %s :: %s" % (service, event.arguments()[0], event.arguments()[1]))
        model.DBSession.flush()
        
    def on_privnotice(self, connect, event):
        service = self.get_service_name(connect)
        logging.info('%s::PRIVNOTICE %s' % (service, ''.join(event.arguments())))
           
    def on_pubmsg(self, connect, event):
        service = self.get_service_name(connect)
        logging.debug(event.target())
        logging.debug(str(service))
        logging.debug(str(self.current_channels))
        if event.target().startswith('#') and event.target()[1:] in self.current_channels[service]:
            logging.debug("logging %s::%s" % (service, ''.join(event.arguments())))
            self.log_message(service, event.source().split('!')[0], event.target()[1:], ''.join(event.arguments()), u'message')
        self.check_nick(connect)
        logging.debug('%s::PUBMSG %s' % (service, ''.join(event.arguments())))
        
    def on_action(self, connect, event):
        service = self.get_service_name(connect)
        if event.target().startswith('#') and event.target()[1:] in self.current_channels[service]:
            self.log_message(service, event.source().split('!')[0], event.target()[1:], ''.join(event.arguments()), u'action')
        self.check_nick(connect)
        
    def on_nick(self, connect, event):
        service = self.get_service_name(connect)
        old_nick = event.source().split('!')[0]
        new_nick = event.target()
        
        self.nicklist[service][new_nick] = list(self.nicklist[service][old_nick])
        del self.nicklist[service][old_nick]
        
        for channel in self.nicklist[service][new_nick]:
            self.log_message(service, old_nick, channel, u"is now known as %s" % new_nick, u'nickchange')
    
    def on_disconnect(self, connect, event):
        logging.debug("Disconnect called...")
        if not self.shutdown_init:
            service = self.get_service_name(connect)
            try:
                connect.quit("Bot disconnected...")
            except irclib.ServerNotConnectedError:
                pass
            self.connect_service(service)
        
    
    ###
    # The logging bit...
    ###
    def log_message(self, service, source, target, message, message_type):
        logging.debug("starting log_message: %s - %s - %s" % (service, target, source))
        channel = model.Channel.by_name_and_service(target, service)
        try:
            source = self.guess_encoding(source)
            message = self.guess_encoding(message)
            msg = model.Message(channel=channel, nick=source, line=message, time=datetime.utcnow(), message_type=message_type)
            model.DBSession.flush()
        except UnicodeError, e:
            pass
        
            
    def guess_encoding(self, data):
        """
        Given a byte string, attempt to decode it.
        Tries the standard 'UTF8' and 'latin-1' encodings,
        Plus several gathered from locale information.

        The calling program *must* first call 
            locale.setlocale(locale.LC_ALL, '')

        If successful it returns 
            decoded_unicode
        If unsuccessful it raises a ``UnicodeError``
        """
        if isinstance(data, unicode):
            # No need to do anything.
            return data
            
        successful_encoding = None
        # we make 'utf-8' the first encoding
        encodings = ['utf-8']
        #
        # next we add anything we can learn from the locale
        try:
            encodings.append(locale.nl_langinfo(locale.CODESET))
        except AttributeError:
            pass
        try:
            encodings.append(locale.getlocale()[1])
        except (AttributeError, IndexError):
            pass
        try:
            encodings.append(locale.getdefaultlocale()[1])
        except (AttributeError, IndexError):
            pass
        #
        # we try 'latin-1' last
        encodings.append('latin-1')
        for enc in encodings:
            # some of the locale calls 
            # may have returned None
            if not enc:
                continue
            try:
                decoded = unicode(data, enc)
                successful_encoding = enc

            except (UnicodeError, LookupError):
                pass
            else:
                break
        if not successful_encoding:
             raise UnicodeError(
            'Unable to decode input data.  Tried the following encodings: %s.'
            % ', '.join([repr(enc) for enc in encodings if enc]))
        else:
             return decoded

if __name__ == "__main__":
    # setup sqlalchemy
    
    from sqlalchemy import create_engine
    from arkivo.model import DBSession
    from ConfigParser import ConfigParser
    
    c = ConfigParser()
    # Temp fix - need to get the current file's directory
    c.read('/var/www/vhosts/turbogears.org/subdomains/irclog/code/arkivo/development.ini')
    #c.read('development.ini')
    
    engine = create_engine(c.get('app:main', 'sqlalchemy.url'), echo=False)
    DBSession.configure(bind=engine)
    
    _TEST_SERVICES = []
    _TEST_SERVICES.append(dict(service="Freenode", server="irc.freenode.net",
                        port=6667, nick="arkivotest"))
    _TEST_SERVICES.append(dict(service="oftc", server="irc.oftc.net", port=6667, nick="arkivotest"))

    _TEST_CHANNELS = [{"service": "Freenode", "channels": ['arkivotest',]},
                    {"service": "oftc", "channels": ['arkivotest',]}]
                    
    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
    stream = logging.StreamHandler()
    #if logfile is not None:
    #    stream = logging.handlers.RotatingFileHandler(logfile, 'a', 1000000, 7)
    stream.setFormatter(formatter)
    logging.getLogger().addHandler(stream)
    
    logging.getLogger().setLevel(logging.INFO)
    
    bot = ArkivoBot()
    
    # Set the locale (for guessing the encoding)
    locale.setlocale(locale.LC_ALL, '')
    
    delay = 5
    
    try:
        while 1:
            if bot.connect():
                logging.info('connecting to server')
                try: bot.start()
                except irclib.ServerNotConnectedError: pass
            logging.info('dropped out of server loop')
            time.sleep(delay)
    finally:
        bot.shutdown()
        logging.info('SYSTEM bot shutdown')
        logging.info('Emailing bot owner')
        
        import smtplib
        now = datetime.now()
        
        fromaddr = "arkivo@splee.co.uk"
        toaddrs = ["spleeman+arkivobot@gmail.com"]
        
        msg = ("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n"
                % (fromaddr, ", ".join(toaddrs), "Arkivobot has died!"))
                
        msg = msg + "Our friend, Arkivobot, has passed away.  Please boot him back to life ASAP."
        
        try:
            server = smtplib.SMTP('localhost')
            result = server.sendmail(fromaddr, toaddrs, msg)
            server.quit()
        except socket.error:
            logging.error("Email failed!")
        
        logging.shutdown()
